﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using NLog;

namespace NGinnBPM.Rules.TableDriven
{
    public class CsvReader
    {
        private TextReader _tr;
        private List<string> _row;
        private int _line = 0;
        private bool _headerRead;
        
        private static Logger log = LogManager.GetCurrentClassLogger();

        public CsvReader(TextReader r)
        {
            Defaults();
            _tr = r;
        }

        private void Defaults()
        {
            Separator = ',';
            TrimValues = true;
            CommentChar = '#';
            Quote = '"';
            HasHeader = false;
            _headerRead = !HasHeader;
        }

        public char Separator { get; set; }
        public bool TrimValues { get; set; }
        public char CommentChar { get; set; }
        public char Quote { get; set; }
        public bool HasHeader { get; set; }
        public string[] Headers { get; set; }

        internal enum Token
        {
            SEP = 0,//separator
            QUOT = 1,//quot mark
            CHAR = 2,//normal character
            WHITE = 3,//whitespace
            CR = 4, //cr
            LF = 5, //lf
            COMMENT = 6, //comment
            END = 7 //no next char
        }

        private Token GetToken(int c)
        {
            if (c < 0)
                return Token.END;
            char ch = (char)c;
            if (c == Separator)
                return Token.SEP;
            if (c == Quote)
                return Token.QUOT;
            if (c == '\r')
                return Token.CR;
            if (c == '\n')
                return Token.LF;
            if (char.IsWhiteSpace(ch))
                return Token.WHITE;
            if (c == CommentChar)
                return Token.COMMENT;
            return Token.CHAR;
        }

        internal enum State
        {
            LineStart, //start of a new line
            FieldStart, //field should begin now
            Field, //inside a field
            QuotField, //inside a quoted field
            Escape, //after a quote, looking for next quote
            EndSpace //whitespace after a field
        }

        public bool Read()
        {
            if (HasHeader && !_headerRead)
            {
                if (!ProcessNextRow())
                    return false;
                _headerRead = true;
                this.Headers = CurrentRow.ToArray();
            }
            bool b = ProcessNextRow();
            return b;
        }


        
        private int NextChar()
        {
            return _tr.Read();
        }

        

        private bool ProcessNextRow()
        {
            _line++;
            StringBuilder curField = new StringBuilder();
            State curState = State.LineStart;
            _row = new List<string>();
            StringBuilder curLine = new StringBuilder();

            try
            {
                do
                {
                    int c = NextChar();
                    Token tok = GetToken(c);
                    char ch = tok == Token.END ? ' ' : (char)c;
                    if (tok != Token.END)
                        curLine.Append(ch);

                    switch (curState)
                    {
                        case State.LineStart:
                            switch (tok)
                            {
                                case Token.COMMENT: //skip line
                                    do
                                    {
                                        Token tw = GetToken(_tr.Read());
                                        if (tw == Token.END || tw == Token.LF || tw == Token.CR)
                                        {
                                            if (tw == Token.CR && GetToken(_tr.Peek()) == Token.LF)
                                                _tr.Read();
                                            break;
                                        }
                                    }
                                    while (true);
                                    _line++;
                                    continue;
                                case Token.END: //end of data
                                    return false;
                                case Token.CR:
                                    if (GetToken(_tr.Peek()) == Token.LF)
                                        _tr.Read();
                                    _line++;
                                    continue;
                                case Token.LF: //skip line
                                    _line++;
                                    continue;
                                case Token.QUOT: //quoted
                                    curState = State.QuotField;
                                    continue;
                                case Token.SEP:
                                    _row.Add(curField.ToString());
                                    curState = State.FieldStart;
                                    continue;
                                case Token.WHITE:
                                    curState = State.FieldStart;
                                    continue;
                                case Token.CHAR:
                                    curField.Append(ch);
                                    curState = State.Field;
                                    continue;
                                default:
                                    throw new Exception();
                            }
                        case State.FieldStart:
                            switch (tok)
                            {
                                case Token.COMMENT:
                                case Token.CHAR:
                                    curField.Append(ch);
                                    curState = State.Field;
                                    continue;
                                case Token.END: //end of data
                                    return true;
                                case Token.CR:
                                case Token.LF: //end line
                                    if (tok == Token.CR && GetToken(_tr.Peek()) == Token.LF)
                                        _tr.Read();
                                    return true;
                                case Token.QUOT: //quoted
                                    curState = State.QuotField;
                                    continue;
                                case Token.SEP:
                                    _row.Add(curField.ToString());
                                    curField = new StringBuilder();
                                    curState = State.FieldStart;
                                    continue;
                                case Token.WHITE:
                                    curState = State.FieldStart;
                                    continue;
                                default:
                                    throw new Exception();
                            }
                        case State.Field:
                            switch (tok)
                            {
                                case Token.COMMENT:
                                case Token.CHAR:
                                case Token.WHITE:
                                    curField.Append(ch);
                                    continue;
                                case Token.END: //end of data
                                    _row.Add(curField.ToString());
                                    return true;
                                case Token.CR:
                                case Token.LF: //skip line
                                    if (tok == Token.CR && GetToken(_tr.Peek()) == Token.LF)
                                        _tr.Read();
                                    _row.Add(curField.ToString());
                                    return true;
                                case Token.QUOT: //quote is a normal char now
                                    curField.Append(ch);
                                    continue;
                                case Token.SEP:
                                    _row.Add(curField.ToString());
                                    curField = new StringBuilder();
                                    curState = State.FieldStart;
                                    continue;
                                default:
                                    throw new Exception();
                            };
                        case State.QuotField:
                            switch (tok)
                            {
                                case Token.CHAR:
                                case Token.COMMENT:
                                case Token.WHITE:
                                case Token.SEP:
                                    curField.Append(ch);
                                    continue;
                                case Token.END: //end of data - unclosed quote
                                    log.Info("Unclosed quote in line {0}", _line);
                                    _row.Add(curField.ToString());
                                    curField = new StringBuilder();
                                    return true;
                                case Token.CR:
                                case Token.LF: //skip line
                                    if (tok == Token.CR && GetToken(_tr.Peek()) == Token.LF)
                                        _tr.Read();
                                    curField.Append('\n');
                                    _line++;
                                    continue;
                                case Token.QUOT: //quoted
                                    curState = State.Escape;
                                    continue;
                                default:
                                    throw new Exception();
                            }
                        case State.Escape:
                            switch (tok)
                            {
                                case Token.QUOT:
                                    curField.Append(ch);
                                    curState = State.QuotField;
                                    continue;
                                case Token.SEP:
                                    _row.Add(curField.ToString());
                                    curField = new StringBuilder();
                                    curState = State.FieldStart;
                                    continue;
                                case Token.WHITE: //eat whitespace now or add a new state
                                    curState = State.EndSpace;
                                    continue;
                                case Token.COMMENT: //skip line
                                case Token.CR:
                                case Token.LF:
                                case Token.CHAR:
                                    log.Info("unexpected char after quote in line {0}", _line);
                                    throw new Exception(string.Format("Unexpected char {0}", c, _line));

                                case Token.END: //end of data
                                    _row.Add(curField.ToString());
                                    return true;
                                default:
                                    throw new Exception();
                            }
                        case State.EndSpace:
                            switch (tok)
                            {
                                case Token.WHITE: //eat whitespace now or add a new state
                                    continue;
                                case Token.SEP:
                                    _row.Add(curField.ToString());
                                    curField = new StringBuilder();
                                    curState = State.FieldStart;
                                    continue;
                                case Token.END:
                                case Token.CR:
                                case Token.LF:
                                    if (tok == Token.CR && GetToken(_tr.Peek()) == Token.LF)
                                        _tr.Read();
                                    _row.Add(curField.ToString());
                                    return true;
                                case Token.QUOT:
                                    curField.Append(ch);
                                    curState = State.QuotField;
                                    continue;
                                case Token.COMMENT: //skip line
                                case Token.CHAR:
                                    log.Info("unexpected char after quote in line {0}", _line);
                                    throw new Exception(string.Format("Unexpected char {0}", c, _line));
                                default:
                                    throw new Exception();
                            }
                        default:
                            throw new Exception();
                    }
                }
                while (true);
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Error in line {0} position {1}: {2} (input: {3})", _line, curLine.Length, ex.Message, curLine.ToString()));
            }
            finally
            {
                if (TrimValues && _row != null)
                {
                    for (int i=0; i<_row.Count; i++)
                        _row[i] = _row[i].Trim();
                }
            }

        }

        
        public IList<string> CurrentRow
        {
            get{return _row;}
        }

        public int LineNumber
        {
            get {return _line;}
        }

        static void Test()
        {
            CsvReader cr = new CsvReader(null);
            while (cr.Read())
            {
                IList<string> row = cr.CurrentRow;
            }
        }
    }
}
